package com.figma.code.connect.models

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind.STRING
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.jsonPrimitive

/**
 * The output of the CodeConnect parser. Contains a list of documents and a list of messages.
 * The messages will be displayed as warnings or errors in the `figma` command line tool.
 */
@Serializable
data class CodeConnectPluginParserOutput(
    val docs: List<CodeConnectDocument>,
    val messages: List<CodeConnectParserMessage> = emptyList(),
)

/**
 * A document that represents a single @FigmaConnect class in a Kotlin file. Refer to the
 * detailed documentation at https://github.com/figma/code-connect for more information on how to
 * annotate a Code Connect file.
 */
@Serializable
data class CodeConnectDocument(
    val component: String?,
    val figmaNode: String,
    val template: String,
    val templateData: TemplateData,
    val variant: Map<
        String,
        @Serializable(with = AnySerializer::class)
        Any,
        >,
    val language: String = "kotlin",
    val label: String = "Compose",
    var sourceLocation: SourceLocation = SourceLocation(file = "", line = 0),
    val source: String = "",
)

/**
 * Describe the level of the message.
 */
@Suppress("unused")
enum class MessageLevel {
    INFO,
    WARNING,
    ERROR,
}

/**
 * Describes the source location of a Compose component.
 *
 * TODO: Implement parsing of Source Locations for Kotlin files.
 */
@Serializable
data class SourceLocation(
    val file: String,
    val line: Int? = null,
)

/**
 * A message that is generated by the CodeConnect parser. These messages will be displayed in the
 * `figma` command line tool.
 */
@Serializable
data class CodeConnectParserMessage(
    val level: MessageLevel,
    val type: String? = null,
    val message: String,
    val sourceLocation: SourceLocation? = null,
)

/**
 * Describes the different modes that are available for the CodeConnect parser.
 */
@Suppress("unused")
enum class Mode {
    PARSE,
    CREATE,
}

enum class ComponentType {
    COMPONENT_SET,
    COMPONENT,
}

@Serializable
data class CodeConnectParserParseInput(
    val mode: Mode,
    val paths: List<String>,
    val autoAddImports: Boolean = true,
)

@Serializable
data class CodeConnectParserCreateInput(
    val mode: Mode,
    val destinationDir: String,
    val destinationFile: String? = null,
    val component: Component,
)

@Serializable
data class Component(
    val id: String,
    val name: String,
    val figmaNodeUrl: String,
    val normalizedName: String,
    val type: ComponentType,
    val componentPropertyDefinitions: Map<String, ComponentPropertyDefinition>? = null,
)

enum class ComponentPropertyDefinitionType {
    BOOLEAN,
    INSTANCE_SWAP,
    TEXT,
    VARIANT,
}

@Serializable
data class ComponentPropertyDefinition(
    val type: ComponentPropertyDefinitionType,
    val defaultValue:
        @Serializable(AnySerializer::class)
        Any,
    val variantOptions: List<String>? = null,
)

@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Any::class)
object AnySerializer : KSerializer<Any> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("Any", STRING)

    override fun serialize(
        encoder: Encoder,
        value: Any,
    ) {
        when (value) {
            is String -> encoder.encodeString(value)
            is Boolean -> encoder.encodeBoolean(value)
            else -> throw SerializationException("Unsupported type ${value::class}")
        }
    }

    override fun deserialize(decoder: Decoder): Any {
        val element = (decoder as JsonDecoder).decodeJsonElement()

        return when {
            element is JsonPrimitive && element.isString -> {
                // handle string
                element.jsonPrimitive.content
            }

            element is JsonPrimitive && element.booleanOrNull != null -> {
                // handle boolean
                element.jsonPrimitive.boolean
            }

            else -> {
                throw SerializationException("Unable to deserialize ${element::class} as it was not a String or Boolean.")
            }
        }
    }
}
